
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
using Red.Controls.Behaviors;

namespace Red.Controls.DataDriven
{
    /// <summary>
    ///     Data-aware editor for numbers
    /// </summary>
    [InteropControl( "Float", "Single", "Double", "float" )]
    public class NumberBox : TextBox
    {
        #region Internal types

        private class SuffixAdder : IValueConverter
        {
            private readonly NumberBox _owner;

            public SuffixAdder( NumberBox owner )
            {
                _owner = owner;
            }

            public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
            {
                string valueStr;
                decimal valueDec = (decimal)(float)value;

                valueStr = valueDec.ToString( _owner._valueFormat );
                return valueStr + _owner.Suffix;
            }

            public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
            {
                float val;
                if ( float.TryParse( ( (string)value ).TrimEnd( _owner.Suffix.ToCharArray() ), out val ) )
                {
                    return val;
                }
                return _owner.Value;
            }
        }

        private class StringToDoubleConverter : IValueConverter
        {
            private readonly NumberBox _owner;

            public StringToDoubleConverter( NumberBox owner )
            {
                _owner = owner;
            }

            public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
            {
                float val;

                if ( float.TryParse( (string)value, out val ) )
                {
                    return val;
                }
                return _owner.Value;
            }

            public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
            {
                decimal valueDec = (decimal)(float)value;

                return valueDec.ToString( _owner._valueFormat );
            }
        }

        private class ProgressIndicator : Adorner
        {
            private double _position = 0f;

            // [0-1]
            public double Position
            {
                get { return _position; }
                set
                {
                    double newPos = ( value < 0.0 ) ? 0.0 : ( value > 1.0 ) ? 1.0 : value; // clamp to [0..1]
                    if ( newPos != _position )
                    {
                        _position = newPos;
                        InvalidateVisual();
                    }
                }
            }

            public ProgressIndicator( NumberBox adornedElement )
                : base( adornedElement )
            {
            }

            protected override void OnRender( DrawingContext drawingContext )
            {
                var box = (NumberBox)AdornedElement;
                Brush brush = new SolidColorBrush( box.ProgressColor );
                double w = box.ActualWidth;
                double h = box.ActualHeight;
                Rect r = new Rect( 0, h-2, w * _position, 2 );
                drawingContext.DrawRectangle( brush, null, r );
            }
        }

        #endregion Internal types

        #region Data

        private static ContextMenu _customContextMenu;
        private readonly ChangeFloatValueWithMouseBehavior _changeFloatValueBeh = new ChangeFloatValueWithMouseBehavior();
        private static RoutedUICommand _resetCmd = new RoutedUICommand( "Reset Value", "resetValue", typeof( NumberBox ) );
        private ProgressIndicator _progress;
        private string _valueFormat;

        #endregion Data

        #region Properties

        public static readonly DependencyProperty XUpdateModifierProperty = 
            DependencyProperty.Register( "XUpdateModifier", typeof( ValueUpdateModifier ), typeof( NumberBox ), new FrameworkPropertyMetadata( ValueUpdateModifier.Decimal ) );

        public static readonly DependencyProperty YUpdateModifierProperty = 
            DependencyProperty.Register( "YUpdateModifier", typeof( ValueUpdateModifier ), typeof( NumberBox ), new FrameworkPropertyMetadata( ValueUpdateModifier.Total ) );

        public static readonly DependencyProperty SuffixProperty = 
            DependencyProperty.Register( "Suffix", typeof( string ), typeof( NumberBox ), new FrameworkPropertyMetadata( string.Empty, SuffixPropertyChanged ) );

        public static readonly DependencyProperty MinProperty =
            DependencyProperty.Register( "Min", typeof( float? ), typeof( NumberBox ), new FrameworkPropertyMetadata( null, MinMaxProperyChanged ) );

        public static readonly DependencyProperty MaxProperty =
            DependencyProperty.Register( "Max", typeof( float? ), typeof( NumberBox ), new FrameworkPropertyMetadata( null, MinMaxProperyChanged ) );

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register( "Value", typeof( float ), typeof( NumberBox ), 
                new FrameworkPropertyMetadata( 0.0f, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, 
                    ValueChanged, null, false, UpdateSourceTrigger.Explicit ) ); // see note in ValueChenged

        public static readonly DependencyProperty ProgressColorProperty =
            DependencyProperty.Register( "ProgressColor", typeof( Color ), typeof( NumberBox ), new FrameworkPropertyMetadata( Color.FromArgb( 128, 255, 255, 255 ) ) );

        public static readonly DependencyProperty MinDigitsAfterCommaProperty =
            DependencyProperty.Register( "MinDigitsAfterComma", typeof( int? ), typeof( NumberBox ), new FrameworkPropertyMetadata( null, DigitsAfterCommaPropertyChanged ) );

        public static readonly DependencyProperty MaxDigitsAfterCommaProperty =
            DependencyProperty.Register( "MaxDigitsAfterComma", typeof( int ), typeof( NumberBox ), new FrameworkPropertyMetadata( 6, DigitsAfterCommaPropertyChanged ) );
        
        /// <summary>
        ///     Color of progress indicator
        /// </summary>
        public Color ProgressColor
        {
            get { return (Color)GetValue( ProgressColorProperty ); }
            set { SetValue( ProgressColorProperty, value ); }
        }

        /// <summary>
        ///     Determines which part of the number changes when using RMB + horizontal drag. Default is Hundredths.
        /// </summary>
        public ValueUpdateModifier XUpdateModifier
        {
            get { return (ValueUpdateModifier)GetValue( XUpdateModifierProperty ); }
            set { SetValue( XUpdateModifierProperty, value ); }
        }

        /// <summary>
        ///     Determines which part of the number changes when using RMB + vertical drag. Default is Decimal.
        /// </summary>
        public ValueUpdateModifier YUpdateModifier
        {
            get { return (ValueUpdateModifier)GetValue( YUpdateModifierProperty ); }
            set { SetValue( YUpdateModifierProperty, value ); }
        }

        /// <summary>
        ///     Determines the unit identifier that should be added at the end of the number
        /// </summary>
        public string Suffix
        {
            get { return (string)GetValue( SuffixProperty ); }
            set { SetValue( SuffixProperty, value ); }
        }

        /// <summary>
        ///     Minimum value accepted by this numberBox
        /// </summary>
        public float? Min
        {
            get { return (float?)GetValue( MinProperty ); }
            set { SetValue( MinProperty, value ); }
        }

        /// <summary>
        ///     Maximum value accepted by this numberBox
        /// </summary>
        public float? Max
        {
            get { return (float?)GetValue( MaxProperty ); }
            set { SetValue( MaxProperty, value ); }
        }

        /// <summary>
        ///     Edited value
        /// </summary>
        public float Value
        {
            get { return (float)GetValue( ValueProperty ); }
            set { SetValue( ValueProperty, value ); }
        }

        /// <summary>
        ///     Minimum digits displayed after comma
        /// </summary>
        public int? MinDigitsAfterComma
        {
            get { return (int?)GetValue( MinDigitsAfterCommaProperty ); }
            set { SetValue( MinDigitsAfterCommaProperty, value ); }
        }

        /// <summary>
        ///     Maximum digits displayed after comma
        /// </summary>
        public int MaxDigitsAfterComma
        {
            get { return (int)GetValue( MaxDigitsAfterCommaProperty ); }
            set { SetValue( MaxDigitsAfterCommaProperty, value ); }
        }

        #endregion Properties

        #region C-tor

        public NumberBox()
        {
            // Text <=> Value
            SetBinding( TextProperty, new Binding( "Value" )
            {
                Mode = BindingMode.TwoWay,
                Source = this,
                Converter = new SuffixAdder( this )
            } );

            // Value <=> Interop (DataContext)
            SetBinding( ValueProperty, new Binding( "Value" )
            {
                Mode = BindingMode.TwoWay,
                Converter = new StringToDoubleConverter(this)
            } );

            SetBinding( IsReadOnlyProperty, new Binding( "IsReadOnly" )
            {
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
            } );

            BindingOperations.SetBinding( _changeFloatValueBeh, ChangeFloatValueWithMouseBehavior.XUpdateModifierProperty, new Binding( "XUpdateModifier" ) { Source = this } );
            BindingOperations.SetBinding( _changeFloatValueBeh, ChangeFloatValueWithMouseBehavior.YUpdateModifierProperty, new Binding( "YUpdateModifier" ) { Source = this } );
            Interaction.GetBehaviors( this ).Add( _changeFloatValueBeh );
            CreateCustomContextMenu();
            ContextMenuOpening += NumberBox_OnContextMenuOpening;
            IsVisibleChanged += NumberBox_IsVisibleChanged;
            UpdateValueFormat();
        }

        #endregion C-tor

        #region Private methods

        private void CreateCustomContextMenu()
        {
            if ( _customContextMenu == null )
            {
                _customContextMenu = new ContextMenu();
                _customContextMenu.Items.Add( new MenuItem() { Header = "Copy Value", Name = "itemCopy", Command = ApplicationCommands.Copy } );
                _customContextMenu.Items.Add( new MenuItem() { Header = "Paste Value", Name = "itemPaste", Command = ApplicationCommands.Paste } );
                _customContextMenu.Items.Add( new Separator() );
                _customContextMenu.Items.Add( new MenuItem() { Header = "Reset Value", Name = "itemResetValue", Command = _resetCmd } );
            }
         
            CommandBindings.Add( new CommandBinding( ApplicationCommands.Copy, Copy_Execute ) );
            CommandBindings.Add( new CommandBinding( ApplicationCommands.Paste, Paste_Execute, Can_Paste ) );
            CommandBindings.Add( new CommandBinding(_resetCmd, ResetValue_Execute ) );

            ContextMenu = _customContextMenu;
        }

        private void UpdateProgressVisibility()
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer( this );
            if ( adornerLayer != null )
            {
                if ( _progress != null )
                {
                    adornerLayer.Remove( _progress );
                }

                if ( Max.HasValue && Min.HasValue )
                {
                    if ( _progress == null )
                    {
                        _progress = new ProgressIndicator( this );
                        UpdateProgressPosition();
                    }
                    adornerLayer.Add( _progress );
                }
            }
        }

        private void UpdateProgressPosition()
        {
            if ( _progress != null )
                _progress.Position = ( Value - Min.Value ) / ( Max.Value - Min.Value );
        }

        private void UpdateValueFormat()
        {
            if ( ( MinDigitsAfterComma == null || MinDigitsAfterComma <= 0 ) && MaxDigitsAfterComma <= 0 )
                return;

            _valueFormat = "0.";
            int k = 0;

            for ( int i = 0; i < MaxDigitsAfterComma; i++ )
            {
                while ( k < MinDigitsAfterComma && k + 1 < MaxDigitsAfterComma )
                {
                    _valueFormat += "0";
                    k++;
                    i++; 
                }
                _valueFormat += "#";
            }

            GetBindingExpression( TextProperty ).UpdateTarget();       
        }

        #endregion Private methods

        #region Event handlers

        private static void DigitsAfterCommaPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            var self = (NumberBox)d;
            self.UpdateValueFormat();
        }

        private void NumberBox_IsVisibleChanged( object sender, DependencyPropertyChangedEventArgs e )
        {
            if ( true.Equals( e.NewValue ) )
            {
                UpdateProgressVisibility();
            }
        }

        private static void MinMaxProperyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            var self = (NumberBox)d;
            self.UpdateProgressVisibility();
        }

        private static void SuffixPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            var box = (NumberBox)d;
            box.GetBindingExpression( TextProperty ).UpdateTarget(); // kick binding to re-apply suffix by calling converter again
        }

        private static void ValueChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            // Unfortunately the only way to pass the _coerced_ value through a biding is to do the whole coercion manually, along with updating the binding 
            // explicitly (see http://stackoverflow.com/a/31831596 with a note that proposed solution still didn't fully work, this one is better)
            var box = (NumberBox)d;
            float val = (float)e.NewValue;

            if ( box.Min.HasValue && val < box.Min.Value )
                box.Value = box.Min.Value; // will re-enter the handler
            else if ( box.Max.HasValue && val > box.Max.Value )
                box.Value = box.Max.Value; // will re-enter the handler
            else
            { 
                // only push through binding if the value is within the scope
                BindingExpression be = box.GetBindingExpression( ValueProperty );
                if ( be != null )
                    be.UpdateSource();

                box.UpdateProgressPosition();
            }
        }

        private void NumberBox_OnContextMenuOpening( object sender, ContextMenuEventArgs e )
        {
            if ( _changeFloatValueBeh != null && ChangeFloatValueWithMouseBehavior.IsUpdatingValue )
            {
                e.Handled = true;
            }
        }

        private void Copy_Execute( object sender, ExecutedRoutedEventArgs e )
        {
            Clipboard.SetText( Value.ToString() );
        }

        private void Can_Paste( object sender, CanExecuteRoutedEventArgs e )
        {
            float number;
            e.CanExecute = float.TryParse( Clipboard.GetText(), NumberStyles.Float, CultureInfo.InvariantCulture, out number );

            if ( e.CanExecute == false )
            e.Handled = true;
        }
 
        private void Paste_Execute( object sender, ExecutedRoutedEventArgs e )
        {
            SelectAll();
            Paste();
        }

        private void Can_Replace( object sender, CanExecuteRoutedEventArgs e )
        {
            float number;
            e.CanExecute = float.TryParse( Clipboard.GetText(), NumberStyles.Float, CultureInfo.InvariantCulture, out number );

            if ( e.CanExecute == false )
                e.Handled = true;
        }    

        private void ResetValue_Execute( object sender, ExecutedRoutedEventArgs e )
        {
            Value = 0;
        }

        #endregion Event handlers

    }

    [InteropControl( "Int", "Int8", "Int16", "Int32", "Uint8", "Uint16", "Uint32", "int" )]
    public class IntNumberBox : NumberBox
    {
        public IntNumberBox()
        {
            XUpdateModifier = ValueUpdateModifier.Total;
            YUpdateModifier = ValueUpdateModifier.Total;
        }
    }
}
